home *** CD-ROM | disk | FTP | other *** search
/ BBS in a Box 3 / BBS in a box - Trilogy III.iso / Files / Prog / U-Z / VideoToolBox Folder / Notes / Luminance.doc < prev    next >
Encoding:
Text File  |  1992-03-11  |  22.5 KB  |  442 lines  |  [TEXT/KAHL]

  1. Luminance.doc
  2.  
  3. HISTORY:
  4. 3/11/92 dgp added prototypes to supplement the explanations of the routines.
  5.  
  6. INTRODUCTION:
  7.  
  8. The purpose of these subroutines is accurate control of contrast of visual 
  9. stimuli for vision experiments. These subroutines set up the Color Lookup Table 
  10. (CLUT) of a video framestore on the Mac II (e.g. the Apple Mac II Color Video 
  11. Card) to properly drive a monochrome monitor (e.g. the Apple High Resolution 
  12. Monochrome Monitor) via an Institute for Sensory Research (ISR) Video 
  13. Attenuator. The ISR Video Attenuator consists of resistive dividers which 
  14. attenuate the three color signal from the video framestore by different 
  15. amounts and combine them to produce one monochrome signal.
  16.  
  17. The video attenuator and the theory behind the algorithms described here are
  18. the topic of a paper:
  19. D.G. Pelli and L. Zhang (1991) Accurate control of contrast on microcomputer displays. 
  20. Vision Research, 31:1337-1360.
  21.  
  22. Achieving the goal of accurate control of contrast required solving five 
  23. problems:
  24. 1. The luminance of the monitor is nonlinearly related to the input voltage which
  25. depends linearly on the triplet 
  26. loaded into the CLUT. This is solved by allowing the user to provide a 
  27. polynomial (I recommend 4th order) describing the nonlinear relationship 
  28. between luminance and nominal voltage. (For computational reasons, you must also supply
  29. a quadratic fit.) The subroutines LToV and VToL use this polynomial 
  30. to convert back and forth. 
  31. 2. Affordable framestores for microcomputers have 8 bit Digital to Analog 
  32. Converters (DACs), which can only represent 256 different luminances. This is 
  33. not enough precision to produce threshold-contrast patterns, which are 
  34. important for vision experiments. This is solved by the ISR Video Attenuator 
  35. which combines the outputs of the three DACs ("red", "green", and "blue") with 
  36. various attenuations to yield one monochrome signal which can represent low 
  37. contrasts (by varying an attenuated DAC) and high contrasts (by varying an 
  38. unattenuated DAC).
  39. 3. DACs are only accurate to ± a least significant bit (LSB). The Brooktree DACs 
  40. used by Apple and RasterOps are specified to have a ±1 LSB "integrated linearity" 
  41. error, which means that the voltage produced by any number will deviate from a 
  42. line connecting the 0 and 255 points by at most 1 LSB, which is defined as 1/255 
  43. times the voltage difference between 0 and 255. I assume a slightly stronger 
  44. restriction will hold, that the difference between any two settings will be in 
  45. error by at most one LSB. So it is important to 
  46. allow the user to specify what range of luminances will be used to present a 
  47. particular stimulus and to fix the coarsest DACs (i.e. those with the least 
  48. attenuation in the ISR Video Attenuator) and only vary the fine DACs to produce 
  49. the specified luminances. The range setting is done by SetLuminanceRange() 
  50. which is called automatically by SetLuminance() and SetLuminances().
  51. 4. Apple's software environment for graphics, QuickDraw, isn't designed for 
  52. vision experiments. The facilities for creating images are fine, but the control 
  53. of the CLUTs provided by the Palette and Color Managers is unsatisfactory for 
  54. vision research because a lot happens behind your back. In particular the 
  55. Palette Manager likes to maintain a consistent color environment across the 
  56. entire desktop, which includes all your screens. Well, in vision experiments we 
  57. generally want to treat each screen as a totally separate device, unaffected by 
  58. what we do to the other screens. This is solved by the routine GDSetEntries() 
  59. which makes a low-level call to the video driver to change the CLUT directly 
  60. without QuickDraw's knowledge. (This will work with any video card 
  61. that works with the Mac II.) The routine LoadLuminances(), which is used below,
  62. is just a convenient glue routine for calling GDSetEntries().
  63. 5. Apple specifies that the video driver should silently implement gamma 
  64. correction to attempt to correct for the display's nonlinear relation between 
  65. luminance and input voltage. We don't want this because this hidden gamma 
  66. correction loses precision and interferes with the operation of the ISR Video 
  67. Attenuator, which will seem to operate NONlinearly if this gamma correction 
  68. takes place unbeknownst to us. This is solved by the routine GDLinearGamma() 
  69. which makes a low-level call to the video driver and loads a linear gamma 
  70. table, i.e. no correction. 
  71.  
  72. SUMMARY:
  73.  
  74. double SetLuminance(GDHandle myGDHandle,luminanceRecord *LP
  75.     ,int theEntry,double luminance
  76.     ,double lowLuminance,double highLuminance)
  77. Set one entry in the ColorSpec table (and the CLUT if myGDHandle is not NULL) to
  78. a specified luminance. It's ok for lowLuminance to be greater than highLuminance.
  79. SetLuminance() sets a single entry to the specified luminance. You must also
  80. indicate the luminance range that you are working over, to allow optimal choice
  81. of which dacs to fix, etc., to yield minimum error in relative luminance. 
  82. SetLuminances() does its work by making a call to SetLuminance() for each entry.
  83. SetLuminance takes 1 ms to set up the range (by calling SetLuminanceRange), which
  84. it only has to do once. Repeated calls to SetLuminance with the same range will
  85. not cause re-computation of the range. Once the range is set, SetLuminance takes
  86. about 0.1 ms. If SetLuminance is too slow for a real-time application you can tell
  87. it to just compute, but not load the new ColorSpec table (just supply NULL in place
  88. of myGDHandle), and you can then quickly load your ColorSpec table into the CLUT
  89. later by calling LoadLuminances().
  90.  
  91. double SetLuminancesAndRange(GDHandle myGDHandle,luminanceRecord *LP
  92.     ,int firstEntry,int lastEntry
  93.     ,double firstLuminance,double lastLuminance
  94.     ,double lowLuminance,double highLuminance)
  95. /*
  96. Set a series of entries in the ColorSpec table (and the CLUT if myGDHandle is not NULL)
  97. to a linear sequence of luminances. 
  98. Uses last two arguments to set the luminance range of interest.
  99. */
  100. double SetLuminances(GDHandle myGDHandle,luminanceRecord *LP
  101.     ,int firstEntry,int lastEntry
  102.     ,double firstLuminance,double lastLuminance)
  103. /*
  104. Set a series of entries in the ColorSpec table (and the CLUT if myGDHandle is not NULL)
  105. to a linear sequence of luminances. Assume this is the entire luminance range of 
  106. interest.
  107. */
  108. SetLuminances() and SetLuminancesAndRange() both produce a linear relationship 
  109. between CLUT entry and luminance over the range firstLuminance to lastLuminance, 
  110. with minimum error relative to any luminance in the range lowLuminance to highLuminance.
  111. (We don't care about a small error in mean luminance.) 
  112. (SetLuminances, which doesn't have these arguments, uses firstLuminance and lastLuminance
  113. to set this range.) The three DACs are linear, and the video attenuator will combine 
  114. them linearly yielding a voltage V at the display input. This allows you to
  115. compute your image (e.g. a sinewave grating) and loaded to the frame store 
  116. as a full scale linear function, i.e. with values ranging 0..255.  Linearization
  117. of the display and setting of contrast to whatever you want are both 
  118. accomplished by one call to SetLuminances(). The contrast can be changed by 
  119. calling SetLuminances() again, now with a larger or smaller luminance range. If 
  120. SetLuminances is too slow for a real-time application (its computations take 
  121. about 8-30 ms to compute a whole 256-entry CLUT) you can tell it to just compute, 
  122. but not load the new ColorSpec table (just supply NULL in place of myGDHandle), and 
  123. you can then quickly load your ColorSpec table into the CLUT later by calling 
  124. LoadLuminances().
  125.  
  126. double GetLuminance(GDHandle myGDHandle,luminanceRecord *LP,int theEntry)
  127. /*
  128. If myGDHandle is not NULL then examines one entry in the actual CLUT, otherwise examines
  129. the ColorSpec table contained in *LP, and in either case
  130. returns the luminance that will be produced. Supplying an illegal entry value results in
  131. a returned value of -INF.
  132. */
  133. GetLuminance() returns the luminance of a single entry. If myGDHandle is not NULL then
  134. GetLuminance will make a low-level call to the video driver to determine what is in that
  135. CLUT entry in the actual hardware, and will return the luminance that is expected
  136. to result given the channel gains and luminance nonlinearity described in your
  137. luminanceRecord. If myGDHandle is NULL then GetLuminance
  138. returns the luminance corresponding to the entry in the ColorSpec table in your
  139. luminanceRecord.
  140.  
  141. void IncrementLuminance(GDHandle myGDHandle,luminanceRecord *LPtr,int theEntry)
  142. /*
  143. Make smallest possible increase of the luminance of one entry in the ColorSpec table.
  144. This is a way to figure out what is the lowest contrast that you can produce.
  145. */
  146.  
  147. void LoadLuminances(GDHandle myGDHandle, luminanceRecord *LP,
  148.     int firstEntry, int lastEntry)
  149. /*
  150. This just calls GDSetEntries() to load your ColorSpec table into the CLUT of
  151. your screen device. It is here simply to provide a cosmetic match to the
  152. call to SetLuminances(), for which loading the CLUT is optional.
  153. Note: if you prefer, instead of LP you may send just the address of 
  154. a ColorSpec table, cast to (luminanceRecord *), since a luminanceRecord 
  155. begins with a ColorSpec table.
  156. */
  157.  
  158. double GetV(GDHandle myGDHandle,luminanceRecord *LP,int theEntry);
  159. double VToL(luminanceRecord *LP,double V);
  160. double LToV(luminanceRecord *LP,double L);
  161. double LToL(luminanceRecord *LP,double L);
  162. Most users will never have any reason to use these routines. They are useful
  163. primarily in error analysis of the Luminance.c package.
  164.  
  165. EXAMPLES:
  166.  
  167. /* load CLUT with linear luminance range, immediately */
  168. tolerance=SetLuminances(myGDHandle,&LR,0,255,(1.-c)*meanLuminance,(1+c)*meanLuminance);
  169.  
  170. /* load ColorSpec table with linear luminance range, then load CLUT */
  171. tolerance=SetLuminances(NULL,&LR,0,255,(1.-c)*meanLuminance,(1+c)*meanLuminance);
  172. LoadLuminance(myGDHandle,&LR,firstEntry,lastEntry);
  173.  
  174. /* load ColorSpec table with sinusoidal luminance range, then load CLUT */
  175. for(entry=0;entry<256;entry++) {
  176.     luminance = meanLuminance*(1.0+c*sin(2.0*3.14159265*entry/256.));
  177.     tolerance=SetLuminance(NULL,&LR,entry,luminance,(1.-c)*meanLuminance,(1+c)*meanLuminance);
  178. }
  179. LoadLuminance(myGDHandle,&LR,firstEntry,lastEntry);
  180.  
  181. /* Examine an entry in the ColorSpec table or CLUT */
  182. luminance=GetLuminance(myGDHandle,&LR,entry);    /* in CLUT */
  183. luminance=GetLuminance(NULL,&LR,entry);        /* in ColorSpec table */
  184.  
  185. NOTES:
  186.  
  187. The argument firstEntry must not exceed lastEntry. They may be equal.
  188.  
  189. The returned value, tolerance, is an estimate of the largest possible error in the
  190. luminance DIFFERENCES, taking into account the precision,
  191. accuracy (assumed to be ±1 least significant bit), and range of the DACs.
  192. (Note the returned tolerance tells you about errors in the luminance DIFFERENCES on
  193. the display. The error in ABSOLUTE luminance of the display is expected to be at
  194. most ±1 step of all the DACs, i.e. one part in 255 of full scale.)
  195.  
  196. There are no restrictions at all on firstLuminance and lastLuminance.
  197. It is reasonable to request an impossibly large luminance range, e.g. from a
  198. negative luminance up to twice the maximum luminance. SetLuminances will produce the
  199. closest possible approximation. The luminance of each entry will be clipped
  200. to fit in the range of possible luminances. Thus you will obtain the requested
  201. contrast for pixel values that aren't clipped. You can detect the clipping by the fact
  202. that the returned tolerance will be very large.
  203.  
  204. A common mistake in C programming is to use a pointer that supposedly points to a 
  205. structure, without ever allocating said structure. Consider the luminanceRecord. 
  206. The following declaration and call will be accepted by the compiler, but will 
  207. usually have disastrous consequences:
  208.     luminanceRecord *LP;
  209.     SetLuminances(...,LP,...);        /* Bad! */
  210. The problem is that you never allocated a luminanceRecord, only a pointer. When
  211. SetLuminances starts storing information in what it thinks is a luminanceRecord it
  212. will actually be writing over a random part of memory. What you should do is:
  213.     luminanceRecord LR;
  214.     SetLuminances(...,&LR,...);        /* Good */
  215. Or you can do this:
  216.     luminanceRecord LR,*LP;
  217.     LP=&LR;
  218.     SetLuminances(...,LP,...);        /* Good */
  219.  
  220. Before using SetLuminance or SetLuminances you must initialize your luminanceRecord. 
  221. I suggest you do that by #including a file with all the parameters describing
  222. your monitor. 
  223.     luminanceRecord LR;
  224.     #include "LuminanceRecord2.h"
  225.  
  226. Here's an example of the contents of a LuminanceRecord2.h file, as produced by the
  227. program CalibrateLuminance. You must use CalibrateLuminance to calibrate
  228. your own framestore, ISR Video Attenuator, and display. 
  229.     LR.screen=2;    /* myGDHandle=GetScreenDevice(LR.screen); */
  230.     LR.date="10:20 AM Wednesday, October 10, 1990";
  231.     LR.id="5111769";
  232.     LR.name="noise";
  233.     LR.notes="denis, lights off";
  234.     LR.dpi=76.0;    /* pixels per inch */
  235.     LR.Hz=66.67;    /* frames per second */
  236.     LR.units="cd/m^2";
  237.     /* coefficients of polynomial fit */
  238.     LR.coefficients=9;    /* # of coefficients in polynomial fit */
  239.     /* L(V)=p[0]+p[1]*V+p[2]*V*V+ . . . ±polynomialError */
  240.     LR.p[0]=-2.28448e-15;
  241.     LR.p[1]=-1.47149e-13;
  242.     LR.p[2]=-8.91074e-12;
  243.     LR.p[3]=-4.47497e-10;
  244.     LR.p[4]=-1.42193e-08;
  245.     LR.p[5]=4.5072e-10;
  246.     LR.p[6]=-1.41595e-12;
  247.     LR.p[7]=-1.16427e-15;
  248.     LR.p[8]=6.69633e-18;
  249.     LR.polynomialError=  0.1974;    /* RMS error of fit */
  250.     /* coefficients of quadratic fit */
  251.     /* L(V)=q[0]+q[1]*V+q[2]*V*V±quadraticError */
  252.     LR.q[0]=5.72078;
  253.     LR.q[1]=-0.261753;
  254.     LR.q[2]=0.00204155;
  255.     LR.quadraticError=  2.1642;    /* RMS error of fit */
  256.     /* coefficients of power law fit */
  257.     /* L(V)=power[0]+Rectify(power[1]+power[2]*V)^power[3]±powerError */
  258.     /* Rectify(x)=x if x≥0, Rectify(x)=0 if x<0 */
  259.     /* Pelli & Zhang (1991) Eqs.9&10 use symbols v=V/255, alpha=power[0], beta=power[1], kappa=power[2]*255, gamma=power[3] */
  260.     LR.power[0]=0.396008;
  261.     LR.power[1]=-2.50082;
  262.     LR.power[2]= 0.035;
  263.     LR.power[3]=2.31643;
  264.     LR.powerError=  0.0863;    /* RMS error of fit */
  265.     /* coefficients of power law fit, with fixed exponent */
  266.     /* L(V)=fixedPower[0]+Rectify(fixedPower[1]+fixedPower[2]*V)^fixedPower[3]±fixedPowerError */
  267.     LR.fixedPower[0]=0.422447;
  268.     LR.fixedPower[1]=-2.68117;
  269.     LR.fixedPower[2]=0.0364552;
  270.     LR.fixedPower[3]=  2.28;
  271.     LR.fixedPowerError=  0.0796;    /* RMS error of fit */
  272.     LR.r=0.0301291;
  273.     LR.g=0.14587;
  274.     LR.b=0.824001;
  275.     LR.gainAccuracy=-0.00241191;
  276.     LR.gm=3.39315;    /* The monitor's contrast gain. */
  277.     LR.VMin=  0;    /* minimum nominal voltage that can be loaded into DAC */
  278.     LR.VMax=255;    /* maximum nominal voltage that can be loaded into DAC */
  279.     LR.LMin=    0.40;    /* luminance at VMin */
  280.     LR.LMax=   74.74;    /* luminance at VMax */
  281.     LR.LBackground=  12.404;    /* background luminance during calibration */
  282.     LR.VBackground=155;    /* background number used during calibration */
  283.     LR.rangeSet=0;    /* indicate that range parameters have yet to be set */
  284.     LR.L.exists=0;    /* indicate that luminance table has yet to be initialized */
  285. Note that these parameters describe fits of three functions to the gamma function.
  286. You may omit any or all of these fits, but should document it by setting the
  287. corresponding error terms to infinity (=1.0/0.0). If you omit all of them then
  288. you must supply a table describing the gamma function, e.g.
  289.     LR.L._VMin=DoubleToMilli(0.);    /* if possible, restrict to just the monotonic range */
  290.     LR.L._VMax=DoubleToMilli(255.);    /* if possible, restrict to just the monotonic range */
  291.     LR.L._dV=(LR.L._VMax-LR.L._VMin)/(LUMINANCES_IN_TABLE-1);
  292.     LR.L.latestIndex=-1;            /* invalid latestIndex, so hunt will start from scratch */
  293.     LR.L.exists=luminanceSet;        /* mark table as valid */
  294.     LR.L._L[0]=DoubleToMilli(0.57);
  295.     LR.L._L[1]=DoubleToMilli(0.57);
  296.     ...
  297.     LR.L._L[255]=DoubleToMilli(101.13);
  298. Normally this table is synthesized at run time, from one of the formulaic descriptions,
  299. but you may prefer to supply it directly, possibly using the raw measurements,
  300. and not bother with any fitting. The formulas are not used if the table is supplied.
  301.  
  302. To fill in the numbers above, you have to do two calibrations. (This is what the
  303. CalibrateLuminance program does.)
  304.  
  305. 0. Before starting, call GDLinearGamma(). (Note that GDOpenWindow 
  306. automatically calls GDLinearGamma for you.) All calibrations should be done
  307. with the ISR Video Attenuator in place.
  308.  
  309. 1. Measure the luminance versus "voltage" nonlinearity of your monitor.
  310. I use a separate program "CalibrateLuminance" to measure the screen luminance at values
  311. of V from 0 to 255. For this calibration you load equal values into Red, Green, and Blue
  312. lookup tables. I use quadratic, polynomial, and power law fits. See Pelli & Zhang (1991).
  313.  
  314. You must set LR.LMin and LR.LMax to the luminances at LR.VMin and LR.VMax.
  315.  
  316. The following program fragment allows you to measure the screen luminance as a function
  317. of nominal voltage.
  318.  
  319. int V,i,done;
  320. double a;
  321.  
  322. #include "LuminanceRecord1.h"
  323. oldGDHandle=GetGDevice();
  324. GetPort(&oldWindowPtr);
  325. LR.screen=1;
  326. myGDHandle=GetScreenDevice(LR.screen);    /* screen 1 */
  327. myCWindowPtr=GDOpenWindow(myGDHandle);    /* Open a full-screen window with explicit colors */
  328. SetPort((WindowPtr)myCWindowPtr);        /* Tell QuickDraw which window */
  329. SetGDevice(myGDHandle);                    /* specify which device's color table */
  330. PmBackColor(1);                            /* pick a color table entry */
  331. EraseRect(&myCWindowPtr->portRect);        /* fill whole window with that color */
  332. SetPort(oldWindowPtr);
  333. SetGDevice(oldGDHandle);
  334. LR.L._VMin=DoubleToMilli(0.);            /* if possible, restrict to just the strictly monotonic range */
  335. LR.L._VMax=DoubleToMilli(255.);
  336. done=0;
  337. for(i=0;;i++){
  338.     LR.L._dV=ceil(MilliToDouble(LR.L._VMax-LR.L._VMin)/(LUMINANCES_IN_TABLE-1));
  339.     V=MilliToLong(i*LR.L._dV);
  340.     if(V>=MilliToLong(LR.L._VMax)){
  341.         V=MilliToLong(LR.L._VMax);
  342.         done=1;
  343.     }
  344.     LR.table[1].rgb.red        =V<<8;    /* Set color table entry 1. */
  345.     LR.table[1].rgb.green    =V<<8;
  346.     LR.table[1].rgb.blue    =V<<8;
  347.     LoadLuminances(myGDHandle,&LR,1,1);    /* Copy color table entry 1 to CLUT */
  348.     printf("%3d Please measure screen luminance now, in %s, and type in:",V,LR.units);
  349.     gets(string);
  350.     sscanf(string,"%lf",&a);
  351.     printf("%6.2g %s\n",a,LR.units);
  352.     LR.L._L[i]=DoubleToMilli(a);
  353.     if(done)break;
  354.     if(LR.L._L[i]<=LR.L._L[0]){    /* restrict to just the strictly monotonic range */
  355.         LR.L._VMin=LongToMilli(V);
  356.         LR.L._L[0]=LR.L._L[i];
  357.         i=0;
  358.     }
  359. }
  360. LR.L.latestIndex=-1;            /* invalid latestIndex, so hunt will start from scratch */
  361. LR.L.exists=luminanceSet;        /* mark table as valid */
  362.  
  363. 2. Measure the gains of the three inputs of your ISR Video Attenuator. You could
  364. do this by using an oscilloscope to measuring the voltage at the input to the monitor.
  365. Instead, I measure the resulting luminance and use LToV() to infer the voltage.
  366. Vary one DAC while holding the other two fixed at 255 and measure the luminances. 
  367. Use the function LToV() to convert back to a "voltage" and compute the gain of the
  368. varying DAC. Note that the three DACs on your framestore will generally have gains that
  369. match to only ±5%. This calibration is measuring the overall gain of each of the three
  370. pathways, including your DACs and the ISR Video Attenuator.    
  371.  
  372. LIMITATIONS:
  373.  
  374. It is imperative that you call GDLinearGamma() at some time before using
  375. SetLuminances. SetLuminances ASSUMES that no gamma correction takes place. You only
  376. need to call GDLinearGamma once. It will stay that way until the next time you
  377. restart your computer. Incidentally, GDOpenWindow() calls GDLinearGamma for you.
  378.  
  379. The luminance calibration data that you supply to SetLuminances must also have been
  380. collected with no gamma correction, i.e. AFTER calling GDLinearGamma. 
  381.  
  382. The reason that gamma correction is not allowed is that it would result in a nonlinear
  383. transformation of the three channels BEFORE they are combined in the Video Attenuator.
  384.  
  385. The measurement of the gains of the three pathways must be made using YOUR framestore.
  386. The gains of your three DACs will in general be different from each
  387. other, and different from framestore to framestore. Brooktree
  388. guarantees the matching of the gains of the three DACs on their chip to only ±5%. 
  389.  
  390. The luminance record may include either a table of luminance calibrations. If it is
  391. not supplied then it will be synthesized from the parameters of the polynomial or power
  392. law fit. If the power, polynomial, or quadratic fit parameters are
  393. not supplied then the appropriate LR.powerError, LR.polynomialError, or LR.quadraticError
  394. field should be set to infinity (=0.0/1.0).
  395.             
  396. The whole package is at present restricted to grayscale. 
  397. There is no provision for linearizing a color monitor. In particular it is assumed that 
  398. the the DACs are linearly combined BEFORE the display nonlinearity. Linearizing luminance
  399. on a color display would need to allow for three different nonlinear transformations.
  400.  
  401. EXPLANATION OF THE CODE:
  402.  
  403. Physically your framestore transforms each CLUT entry number to a voltage that will be
  404. linearly related to the number. The three output voltages will be combined by the ISR
  405. Video Attenuator to produce a single voltage which drives the video monitor. Finally,
  406. the display nonlinearly transforms V to produce a luminance L. My convention for 
  407. "measuring" the "voltage" V at the input of the monitor is that V=0 when all three
  408. DACs are set to zero and V=255 when all three DACs are set to 255. (This will be linearly
  409. related to volts measured, with a voltmeter, at the output of the Video Attenuator.)
  410. The virtue of the attenuator is that it allows us to produce nonintegral values of V.
  411.  
  412. There are two givens:
  413. 1. DACs are inaccurate. A good DAC may be specified to be merely monotonic. For purposes
  414. of computing the returned tolerance value, I follow the Brooktree DAC specification of
  415. ±1 LSB error in integrated linearity and assume
  416. that the error in any luminance difference is at most ±1 LSB.
  417. 2. We want to minimize the error in representing the waveform, but are not particularly
  418. worried about the exact value of the mean luminance. Thus, if we have DACs with different
  419. gains we may fix the coarse DACs to set the mean luminance and vary the fine DACs to
  420. produce the linear range of luminances requested.
  421.  
  422. Here's the strategy. We want to cover the range L0 to Ln with the smallest possible
  423. error in L-L0. 
  424.  
  425. Steps 1 to 5 happen in SetLuminanceRange:
  426. 1. Sort the DACs by gain g, where gain is defined as the change in V when
  427. a single DAC is increased from 0 to 255, so g0+g1+g2=1. Let the gains be g0>g1>g2. 
  428. 2. Transform the luminance range to a nominal voltage range lowV and highV.
  429. 3. Decide which DACs should be variable and which should be fixed,
  430. so as the minimize the tolerance in the luminance increments.
  431. 4. Temporarily set the variable DACs to their midpoints. Now set the fixed DACs to
  432. most accurately represent the midpoint of the nominal voltage range, (lowV+highV)/2.
  433. 5. If necessary, compute a small luminance offset LShift to bring the requested range 
  434. lowV to highV into the range attainable by the variable DACs. (This is necessary
  435. because the centering in step 4 may not be precise enough.)
  436.  
  437. Step 6 happens in _SetLuminance:
  438. 6. Set the variable DACs in the ColorSpec entry so as to most accurately represent
  439. L+LShift.
  440.  
  441. SetLuminances(), SetLuminancesAndRange(), and SetLuminance() all call _SetLuminance().
  442.